package com.jl.js.core;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.eclipse.wst.jsdt.debug.rhino.debugger.RhinoDebugger;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.FunctionObject;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;

import com.jl.js.filepath.IJsFilePath;
import com.jl.js.filepath.FilePathUtil;

public class Engine {
	/** ID associate the globa scope */
	protected final static Integer ID_ENGINE = new Integer(65473652);
	/** the print stream uesed to print message to console */
	private PrintStream ps;
	/** global scope */
	private ScriptableObject globalScope;

	private ContextFactory factory;
	private Context cx;
	private RhinoDebugger debugger;
	
	public Engine(PrintStream ps, String port) {
		this.ps = ps;
		factory = new ContextFactory();
		initDebugger(port);
		cx = factory.enterContext();
		cx.setWrapFactory(new JsWrapFactory());
		globalScope = cx.initStandardObjects(null, true);
		globalScope.associateValue(ID_ENGINE, this);

		Method[] methods = this.getClass().getMethods();
		addStaticMethod(methods, "load", globalScope);
		addStaticMethod(methods, "print", globalScope);
		addStaticMethod(methods, "defineClass", globalScope);
		
	}

	/**
	 * return the global scope
	 * 
	 * @return
	 */
	public ScriptableObject getScope() {
		return globalScope;
	}

	/**
	 * add a scriptableObjext to the context
	 * 
	 * @param cx
	 * @param thisObj
	 * @param args
	 * @param funObj
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws InvocationTargetException
	 * @throws ClassNotFoundException
	 */
	@SuppressWarnings("unchecked")
	public static void defineClass(Context cx, Scriptable thisObj,
			Object[] args, Function funObj) throws IllegalAccessException,
			InstantiationException, InvocationTargetException,
			ClassNotFoundException {
		Class<?> clazz = null;
		try {
			clazz = Class.forName(Context.toString(args[0]));
			AbsolutelyFileClassLoader.setParentLoader(clazz.getClassLoader());
		} catch (ClassNotFoundException e) {
			AbsolutelyFileClassLoader loader = new AbsolutelyFileClassLoader(
					new File("ScriptClass"));
			clazz = loader.loadClass(Context.toString(args[0]));
		}
		ScriptableObject.defineClass(thisObj, (Class<Scriptable>) clazz);
	}

	/**
	 * load file from disk
	 * 
	 * @param cx
	 * @param thisObj
	 * @param args
	 * @param funObj
	 * @throws IOException
	 */
	public static void load(Context cx, Scriptable thisObj, Object[] args,
			Function funObj) throws IOException {

		Engine engine = Engine.getEngine(thisObj);

		for (int i = 0; i < args.length; i++) {
			engine.executeScriptFile(cx, thisObj, Context.toString(args[i]));
		}
	}

	/**
	 * print stream to console
	 * 
	 * @param cx
	 * @param thisObj
	 * @param args
	 * @param funObj
	 */
	public static void print(Context cx, Scriptable thisObj, Object[] args,
			Function funObj) {
		Engine engine = Engine.getEngine(thisObj);
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < args.length; i++) {
			sb.append(args[i].toString());
		}
		engine.ps.print(sb.toString());
	}

	/**
	 * execute the file
	 * 
	 * @param cx
	 * @param scope
	 * @param scriptName
	 * @return
	 * @throws IOException
	 */
	public Object executeScriptFile(Context cx, Scriptable scope,
			String scriptName) throws IOException {
		BufferedReader reader = null;
		IJsFilePath path = FilePathUtil.getJSFilePath(scriptName);
		try {
			reader = new BufferedReader(new FileReader(path.getFile()));
			StringBuilder sb = new StringBuilder();
			String line = reader.readLine();
			while (line != null) {
				sb.append(line).append('\n');
				line = reader.readLine();
			}

			Script script = cx.compileString(sb.toString(), path.getAbsolutePath(),
					1, null);

			Object result = script.exec(cx, scope);
			// if ((result instanceof Function) && !(result instanceof
			// XMLObject)) {
			// Function fo = (Function) result;
			// Object lastresult = scope.get("lastresult", scope);
			// if (lastresult != null) {
			// Object args[] = { lastresult };
			// result = fo.call(cx, scope, scope, args);
			// } else {
			// Object args[] = {};
			// result = fo.call(cx, scope, scope, args);
			// }
			// }
			return result;
		} catch (IOException e) {
			if (reader != null) {
				try {
					reader.close();
				} catch (Exception e2) {
					e2.printStackTrace();
				}
			}
			throw e;
		}
	}

	/**
	 * add a java objext to the scope
	 * 
	 * @param name
	 * @param obj
	 */
	public void addJavaObject(String name, Object obj) {
		Object jsObj = Context.javaToJS(obj, globalScope);
		ScriptableObject.putProperty(globalScope, name, jsObj);
	}

	// private method

	/**
	 * return the top scope
	 * 
	 * @param o
	 * @return
	 */
	private static Engine getEngine(Scriptable o) {
		Engine engine = (Engine) ScriptableObject
				.getTopScopeValue(o, ID_ENGINE);
		return engine;
	}

	private void addStaticMethod(Method[] methods, String name,
			ScriptableObject scope) {
		Method m = findMethod(methods, name);
		FunctionObject func = new FunctionObject(name, m, scope);
		scope.defineProperty(name, func, ScriptableObject.DONTENUM);
	}

	private Method findMethod(Method[] methods, String name) {
		for (int i = 0, n = methods.length; i < n; i++) {
			Method m = methods[i];
			if (name.equals(m.getName())) {
				return m;
			}
		}
		return null;
	}

	public void initDebugger(String port) {
		String rhino = "transport=socket,suspend=y,address=" + port;
		debugger = new RhinoDebugger(rhino);
		factory.addListener(debugger);
	}

	public void startDebug() {
		Thread t = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					debugger.start();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
		t.start();
	}
	
	public Context getContext(){
		return this.cx;
	}

}
